/*
 * Copyright (C) 2007-2012 by XDEV Software, All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 3.0 as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package xdev.ui.text;


import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.LineMetrics;
import java.beans.Beans;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import javax.swing.JOptionPane;
import javax.swing.event.DocumentEvent;
import javax.swing.text.AttributeSet;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.StyleConstants;
import javax.swing.text.rtf.RTFEditorKit;

import xdev.Application;
import xdev.ui.UIUtils;
import xdev.ui.XdevApplicationContainer;
import xdev.ui.XdevTextComponent;
import xdev.ui.event.TextChangeAdapter;
import xdev.util.logging.LoggerFactory;
import xdev.util.logging.XdevLogger;


public class XdevDocument
{
	/**
	 * Logger instance for this class.
	 */
	private static final XdevLogger	log						= LoggerFactory
																	.getLogger(XdevDocument.class);
	
	public final static int			LEFT					= 0;
	public final static int			RIGHT					= 1;
	public final static int			CENTER					= 2;
	public final static int			TRAILING				= 3;
	public final static int			JUSTIFIED				= TRAILING;
	
	private final static int		TAB_WIDTH				= 30;
	
	public final static Object		LINK_ATTRIBUTE			= "LINK";
	
	public final static int			FIT_NONE				= 0;
	public final static int			FIT_STRETCH				= 1;
	public final static int			FIT_FIT					= 2;
	
	public final static String		CONTENT_TYPE_TEXT_PLAIN	= "text/plain";
	public final static String		CONTENT_TYPE_TEXT_RTF	= "text/rtf";
	// public final static String CONTENT_TYPE_TEXT_HTML = "text/html";
	
	private XdevTextComponent		txtCpn;
	
	private String					text;
	private String					contentType				= CONTENT_TYPE_TEXT_PLAIN;
	
	private Sign[]					signs;
	private Word[]					words;
	private PaintSet[]				paintSet;
	private Rectangle				place;
	
	private List<TextChangeAdapter>	listeners				= new Vector();
	
	
	public XdevDocument(XdevTextComponent txtCpn)
	{
		this.txtCpn = txtCpn;
		
		Font font = txtCpn.getFont();
		Color color = txtCpn.getForeground();
		boolean underline = txtCpn.getUnderline();
		char ch = '\n';
		FontRenderContext frc = getFontRenderContext();
		FontMetrics fm = getComponent().getFontMetrics(font);
		LineMetrics lm = font.getLineMetrics("" + ch,frc);
		Char s = new Char(ch,font,fm.getAscent(),fm.getDescent(),fm.getLeading(),color,underline,
				(int)lm.getUnderlineOffset(),(int)lm.getUnderlineThickness());
		signs = new Sign[]{s};
	}
	
	
	private Component getComponent()
	{
		Component cpn = txtCpn.getCpn();
		if(cpn != null && cpn.isShowing())
		{
			return cpn;
		}
		
		if(!Beans.isDesignTime())
		{
			XdevApplicationContainer container = Application.getContainer();
			if(container != null)
			{
				cpn = (Component)container;
				if(cpn != null && cpn.isShowing())
				{
					return cpn;
				}
			}
		}
		
		return JOptionPane.getRootFrame();
	}
	
	
	private XdevDocument(Sign[] signs, XdevTextComponent txtCpn)
	{
		this.txtCpn = txtCpn;
		this.signs = signs;
	}
	
	
	public XdevDocument copy(XdevTextComponent txtCpn)
	{
		return new XdevDocument(signs,txtCpn);
	}
	
	
	public void addChangeListener(TextChangeAdapter listener)
	{
		listeners.add(listener);
	}
	
	
	public void removeChangeListener(TextChangeAdapter listener)
	{
		listeners.remove(listener);
	}
	
	
	protected void fireTextChanged()
	{
		int c = listeners.size();
		if(c > 0)
		{
			final int length = length();
			DocumentEvent event = new DocumentEvent()
			{
				@Override
				public int getOffset()
				{
					return 0;
				}
				
				
				@Override
				public int getLength()
				{
					return length;
				}
				
				
				@Override
				public Document getDocument()
				{
					return null;
				}
				
				
				@Override
				public EventType getType()
				{
					return EventType.CHANGE;
				}
				
				
				@Override
				public ElementChange getChange(Element elem)
				{
					return null;
				}
				
			};
			for(TextChangeAdapter listener : listeners)
			{
				listener.textChanged(event);
			}
		}
	}
	
	
	public int length()
	{
		return signs.length;
	}
	
	
	public Sign getSign(int index)
	{
		return signs[index];
	}
	
	
	public void paint(Graphics g0)
	{
		if(signs == null)
		{
			return;
		}
		
		if(paintSet == null)
		{
			if(text != null)
			{
				setText(text,contentType);
			}
			
			createPaintSets();
		}
		
		Graphics2D g = (Graphics2D)g0;
		
		RenderingHints hints = (RenderingHints)g.getRenderingHints().clone();
		UIUtils.setQualityHints(g);
		
		Shape clip = g.getClip();
		
		for(int i = 0; paintSet != null && i < paintSet.length; i++)
		{
			paintSet[i].paint(g,clip);
		}
		
		g.setRenderingHints(hints);
	}
	
	
	private void createPaintSets()
	{
		List<PaintSet> list = new ArrayList();
		int index = 0;
		Sign s = signs[0];
		for(int i = 0; i < signs.length - 1; i++)
		{
			if(signs[i] instanceof TextImage)
			{
				if(index < i)
				{
					list.add(new PaintSet(index,i,(Char)s));
					s = null;
				}
				list.add(new PaintSet((TextImage)signs[i]));
				index = i + 1;
			}
			else
			{
				Char c = (Char)signs[i];
				if(s == null)
				{
					s = c;
				}
				if(c.isTab() || c.isSpace())
				{
					if(index < i)
					{
						list.add(new PaintSet(index,i,(Char)s));
					}
					int ti = i;
					while(ti < signs.length - 1 && (signs[ti].isTab() || signs[ti].isSpace()))
					{
						list.add(new PaintSet((Char)signs[i]));
						ti++;
					}
					i = index = ti;
					if(i < signs.length - 1 && signs[i] instanceof Char)
					{
						s = signs[i];
					}
					else
					{
						i--;
					}
				}
				else if(!signs[i].equalsStyleAndRow(s))
				{
					if(s instanceof TextImage)
					{
						list.add(new PaintSet((TextImage)s));
						index = i + 1;
					}
					else
					{
						list.add(new PaintSet(index,i,(Char)s));
						index = i;
					}
					s = signs[i];
				}
			}
		}
		if(index < signs.length - 1)
		{
			if(s instanceof TextImage)
			{
				list.add(new PaintSet((TextImage)s));
			}
			else
			{
				list.add(new PaintSet(index,signs.length - 1,(Char)s));
			}
		}
		
		paintSet = list.toArray(new PaintSet[list.size()]);
	}
	
	
	
	private class PaintSet
	{
		private SignBounds	outline;
		private boolean		underline;
		private int			x, y, uX, uY, uX2;
		private Char		s;
		private char[]		chars;
		private TextImage	image;
		
		
		public PaintSet(TextImage s)
		{
			image = s;
			outline = new SignBounds(s.rect.x,s.rect.y,s.rect.width,s.rect.height);
		}
		
		
		public PaintSet(Char s)
		{
			this.s = s;
			
			chars = new char[0];
			
			x = s.rect.x;
			y = s.drawY;
			
			underline = s.underline;
			if(underline)
			{
				uX = s.rect.x;
				uY = s.drawY + s.underlineY + 1;
				uX2 = Math.round(s.rect.x + s.rect.width);
			}
			
			outline = new SignBounds(s.rect.x,s.drawY,s.rect.width,s.rect.height);
		}
		
		
		public PaintSet(int start, int end, Char s)
		{
			this.s = s;
			
			outline = s.getBounds();
			
			int ci = 0;
			chars = new char[end - start];
			for(int i = start; i < end; i++, ci++)
			{
				chars[ci] = getSign(i).c;
				getSign(i).addBounds(outline);
			}
			
			x = s.rect.x;
			y = s.drawY;
			
			underline = s.underline;
			if(underline)
			{
				uX = s.rect.x;
				uY = s.drawY + s.underlineY + 1;
				Char us = getSign(end - 1);
				uX2 = Math.round(us.rect.x + us.rect.width);
			}
		}
		
		
		private Char getSign(int i)
		{
			return (Char)signs[i];
		}
		
		private GlyphVector	glyphVector;
		
		
		public void paint(Graphics2D g, Shape clip)
		{
			if(clip == null || clip.intersects(outline.x,outline.y,outline.width,outline.height))
			{
				if(image != null)
				{
					image.paint(g);
				}
				else
				{
					if(glyphVector == null && chars.length > 0)
					{
						glyphVector = s.font.createGlyphVector(g.getFontRenderContext(),chars);
					}
					
					g.setColor(s.color);
					
					if(glyphVector != null)
					{
						g.drawGlyphVector(glyphVector,x,y);
					}
					
					if(underline)
					{
						g.drawLine(uX,uY,uX2,uY);
					}
				}
			}
		}
	}
	
	
	public Link getSignLinkForPoint(Point p)
	{
		SignBounds r;
		for(int i = 0; i < signs.length; i++)
		{
			r = signs[i].getBounds();
			if(signs[i] != null && r != null && r.getBounds().contains(p))
			{
				return signs[i].getLink();
			}
		}
		return null;
	}
	private boolean	fire	= true;
	
	
	public void recalc()
	{
		if(text == null)
		{
			text = toString();
		}
		
		fire = false;
		int h = Math.max(setText(text,contentType,FIT_STRETCH),txtCpn.getOriginalSize().height);
		fire = true;
		
		Dimension dim = new Dimension(txtCpn.getPreferredSize().width,h);
		txtCpn.setPreferredSize(dim);
	}
	
	
	public int setText(String text)
	{
		return setText(text,CONTENT_TYPE_TEXT_PLAIN);
	}
	
	
	public int setText(String text, String contentType)
	{
		return setText(text,contentType,FIT_NONE);
	}
	
	
	public int setText(String text, int fit)
	{
		return setText(text,contentType,fit);
	}
	
	
	public int setText(String text, String contentType, int fit)
	{
		this.text = text;
		this.contentType = contentType;
		
		txtCpn.setPaintText();
		// txtCpn.setTextColumnCount(1);
		
		Color color = txtCpn.getForeground();
		Font font = txtCpn.getFont();
		boolean underline = txtCpn.getUnderline();
		
		int len = text.length();
		
		Link link = null;
		FontRenderContext frc = getFontRenderContext();
		
		if(CONTENT_TYPE_TEXT_PLAIN.equals(contentType))
		{
			text = text.concat("\n");
			len = text.length();
			
			FontMetrics fm = getComponent().getFontMetrics(font);
			int height = fm.getHeight();
			int ascent = fm.getAscent();
			int descent = fm.getDescent();
			int leading = fm.getLeading();
			
			Sign[] signs = new Sign[len];
			for(int i = 0; i < len; i++)
			{
				char ch = text.charAt(i);
				
				LineMetrics lm = font.getLineMetrics("" + ch,frc);
				signs[i] = new Char(ch,font,ascent,descent,leading,color,underline,
						(int)lm.getUnderlineOffset(),(int)lm.getUnderlineThickness());
				signs[i].getBounds().setBounds(0,0,
						ch == '\n' ? 2 : ch == '\t' ? 0 : fm.charWidth(ch),height);
				signs[i].setLink(link);
			}
			
			this.signs = signs;
		}
		else if(CONTENT_TYPE_TEXT_RTF.equals(contentType))
		{
			DefaultStyledDocument dsd = new DefaultStyledDocument();
			RTFEditorKit rtf = new RTFEditorKit();
			try
			{
				rtf.read(new StringReader(text),dsd,0);
				
				text = dsd.getText(0,dsd.getLength());
				text = text.concat("\n");
				len = text.length();
				Sign[] signs = new Sign[len];
				
				for(int i = 0; i < len; i++)
				{
					AttributeSet as = dsd.getCharacterElement(i).getAttributes();
					char ch = text.charAt(i);
					String fontName = StyleConstants.getFontFamily(as);
					int fontStyle = Font.PLAIN;
					if(StyleConstants.isBold(as))
					{
						fontStyle += Font.BOLD;
					}
					if(StyleConstants.isItalic(as))
					{
						fontStyle += Font.ITALIC;
					}
					int fontSize = StyleConstants.getFontSize(as);
					
					color = StyleConstants.getForeground(as);
					underline = StyleConstants.isUnderline(as);
					
					font = new Font(fontName,fontStyle,fontSize);
					
					FontMetrics fm = getComponent().getFontMetrics(font);
					int height = fm.getHeight();
					int ascent = fm.getAscent();
					int descent = fm.getDescent();
					int leading = fm.getLeading();
					
					LineMetrics lm = font.getLineMetrics("" + ch,frc);
					signs[i] = new Char(ch,font,ascent,descent,leading,color,underline,
							(int)lm.getUnderlineOffset(),(int)lm.getUnderlineThickness());
					signs[i].getBounds().setBounds(0,0,
							ch == '\n' ? 2 : ch == '\t' ? 0 : fm.charWidth(ch),height);
				}
				
				this.signs = signs;
			}
			catch(Exception e)
			{
				// BadLocation or I/O - shouldn't happen
				log.error(e);
			}
		}
		else
		{
			throw new IllegalArgumentException("content type not supported: " + contentType);
		}
		
		int retVal = relayout(fit);
		
		fireTextChanged();
		
		return retVal;
	}
	
	
	public int relayout(int fit)
	{
		place = new Rectangle(txtCpn.getSize());
		
		Insets bi = txtCpn.getBorderInsets(true);
		
		place.x += bi.left;
		place.y += bi.top;
		place.width -= (bi.left + bi.right + 1);
		place.height -= (bi.top + bi.bottom);
		
		if(txtCpn.isVertical())
		{
			int w = place.width;
			place.width = place.height;
			place.height = w;
		}
		
		List<Word> list = new ArrayList();
		Word word = new Word();
		boolean addLast = false;
		
		for(int i = 0; i < signs.length; i++)
		{
			if(signs[i].canBreak() || signs[i].isTab() || signs[i].isBreak())
			{
				if(word.signs.size() > 0)
				{
					list.add(word);
				}
				word = new Word();
				word.signs.addElement(signs[i]);
				list.add(word);
				word = new Word();
				addLast = false;
			}
			else
			{
				word.signs.addElement(signs[i]);
				addLast = true;
			}
		}
		if(addLast)
		{
			list.add(word);
		}
		
		int wordCount = list.size();
		words = new Word[wordCount];
		for(int i = 0; i < wordCount; i++)
		{
			words[i] = list.get(i);
			words[i].computeSize();
		}
		
		boolean morePlace = true;
		int y = place.y, curRow = 0;
		
		if(fit != FIT_NONE)
		{
			place.height = 10000;
		}
		
		int align = txtCpn.getHorizontalAlign();
		
		int curWord = 0;
		Sign lastSign = null;
		
		while(curWord < words.length && morePlace)
		{
			int rowWidth = 0, rowHeight = 0;
			
			for(int wi = curWord; wi < words.length; wi++)
			{
				if(rowWidth + words[wi].width <= place.width || words[wi].isSpace())
				{
					rowWidth += words[wi].width;
					rowHeight = Math.max(rowHeight,words[wi].height);
				}
				else
				{
					break;
				}
			}
			
			Rectangle rowRect = new Rectangle(place.x,y,place.width,rowHeight);
			rowHeight = 0;
			
			int usedWidth = 0;
			RowBox rowBox = null;
			boolean next = true, doBreak = false;
			
			while(next && curWord < words.length)
			{
				if(words[curWord].isBreak())
				{
					doBreak = true;
					next = false;
					rowHeight = Math.max(words[curWord++].height,rowHeight);
				}
				else
				{
					float ww = words[curWord].width;
					if(words[curWord].isTab())
					{
						ww = TAB_WIDTH - ((rowRect.x + usedWidth) % TAB_WIDTH);
						usedWidth += ww;
						words[curWord].width = words[curWord].sign[0].getBounds().width = ww;
					}
					if(usedWidth + ww < rowRect.width || words[curWord].isSpace())
					{
						usedWidth += ww;
						rowHeight = Math.max(rowHeight,words[curWord].height);
						if(rowBox == null)
						{
							rowBox = new RowBox(rowRect);
						}
						rowBox.addWord(words[curWord]);
						curWord++;
					}
					else
					{
						next = false;
					}
				}
			}
			
			if(rowHeight > 0 && rowBox != null)
			{
				rowRect.height = rowHeight;
				
				lastSign = rowBox.layout(align,rowHeight,curRow);
				
				curRow++;
				y += rowHeight;
			}
			else if(doBreak)
			{
				curRow++;
				y += rowHeight;
			}
			else
			{
				if(rowRect != null && words[curWord].width >= rowRect.width
						&& words[curWord].sign[0].getBounds().width < rowRect.width)
				{
					float tw = words[curWord].width;
					Word newWord = new Word();
					
					while(words[curWord].signs.size() > 0)
					{
						if(tw >= rowRect.width)
						{
							Sign ts = (Sign)words[curWord].signs.elementAt(words[curWord].signs
									.size() - 1);
							words[curWord].signs.removeElement(ts);
							newWord.signs.insertElementAt(ts,0);
							tw -= ts.getBounds().width;
						}
						else
						{
							break;
						}
					}
					
					words[curWord].computeSize();
					newWord.computeSize();
					
					int wc = words.length;
					Word[] oldWords = words;
					words = new Word[wc + 1];
					System.arraycopy(oldWords,0,words,0,curWord + 1);
					System.arraycopy(oldWords,curWord + 1,words,curWord + 2,wc - curWord - 1);
					words[curWord + 1] = newWord;
				}
				else
				{
					y++;
				}
			}
			
			if(y >= place.y + place.height)
			{
				morePlace = false;
			}
		}
		
		paintSet = null;
		
		if(lastSign == null)
		{
			if(fire)
			{
				txtCpn.repaint();
			}
			
			return 1;
		}
		
		Dimension d = txtCpn.getSize();
		int h = lastSign.getBounds().y + lastSign.getBounds().height + bi.bottom;
		if(fit == FIT_STRETCH)
		{
			h = Math.max(h,d.height);
		}
		
		if(fire)
		{
			if(fit != FIT_NONE && lastSign != null)
			{
				Dimension dim = new Dimension(d.width,h);
				txtCpn.setSize(dim);
				txtCpn.setPreferredSize(dim);
				
				txtCpn.repaint();
			}
		}
		
		return h;
	}
	
	
	private FontRenderContext getFontRenderContext()
	{
		Component cpn = getComponent();
		Graphics graphics = cpn != null ? cpn.getGraphics() : null;
		return graphics != null ? ((Graphics2D)graphics).getFontRenderContext()
				: new FontRenderContext(null,false,false);
	}
	
	
	@Override
	public String toString()
	{
		StringBuffer sb = new StringBuffer();
		for(int i = 0; i < signs.length - 1; i++)
		{
			sb.append(signs[i].toString());
		}
		return sb.toString();
	}
	
	
	public int getPlaceWidth()
	{
		Insets bi = txtCpn.getBorderInsets(true);
		return txtCpn.getSize().width - (bi.left + bi.right + 1);
	}
	
	
	public Font getFont()
	{
		return txtCpn.getFont();
	}
	
	
	public Color getForeground()
	{
		return txtCpn.getForeground();
	}
	
	
	public Insets getBorderInsets()
	{
		return txtCpn.getBorderInsets(true);
	}
	
	
	public int getHorizontalAlign()
	{
		return txtCpn.getHorizontalAlign();
	}
	
	
	public boolean isVertical()
	{
		return txtCpn.isVertical();
	}
	
	
	public int getVerticalAlign()
	{
		return txtCpn.getVerticalAlign();
	}
	
	
	public String[] getLines()
	{
		if(signs.length == 0)
		{
			return new String[0];
		}
		
		Map<Integer, List<String>> map = new Hashtable();
		int maxRow = 0;
		for(int i = 0; i < words.length; i++)
		{
			maxRow = Math.max(maxRow,words[i].row);
			Integer key = new Integer(words[i].row);
			List list;
			if(map.containsKey(key))
			{
				list = map.get(key);
			}
			else
			{
				list = new ArrayList();
				map.put(key,list);
			}
			list.add(words[i]);
		}
		
		List<String> list = new ArrayList(maxRow);
		for(int i = 0; i <= maxRow; i++)
		{
			List<String> row = map.get(i);
			if(row == null)
			{
				list.add("");
			}
			else
			{
				StringBuffer sb = new StringBuffer();
				for(int c = 0; c < row.size(); c++)
				{
					sb.append(row.get(c));
				}
				list.add(sb.toString());
			}
		}
		
		return list.toArray(new String[list.size()]);
	}
}
